import { type Dom, isModifierKeyMatch, type ModifierKey } from '../../common'
import { Config } from '../../config'
import { Point, type PointLike } from '../../geometry'
import type { Graph } from '../../graph'
import type { Edge } from '../../model/edge'
import type { EdgeView } from '../../view/edge'
import { ToolItem, type ToolItemOptions } from '../../view/tool'
import { View } from '../../view/view'
import { createViewElement } from '../../view/view/util'
import type { SimpleAttrs } from '../attr'

const pathClassName = Config.prefix('edge-tool-vertex-path')
export class Vertices extends ToolItem<EdgeView, Options> {
  public static defaults: Options = {
    ...ToolItem.getDefaults(),
    name: 'vertices',
    snapRadius: 20,
    addable: true,
    removable: true,
    removeRedundancies: true,
    stopPropagation: true,
    attrs: {
      r: 6,
      fill: '#333',
      stroke: '#fff',
      cursor: 'move',
      'stroke-width': 2,
    },
    createHandle: (options) => new Handle(options),
    markup: [
      {
        tagName: 'path',
        selector: 'connection',
        className: pathClassName,
        attrs: {
          fill: 'none',
          stroke: 'transparent',
          'stroke-width': 10,
          cursor: 'pointer',
        },
      },
    ],
    events: {
      [`mousedown .${pathClassName}`]: 'onPathMouseDown',
      [`touchstart .${pathClassName}`]: 'onPathMouseDown',
    },
  }

  protected handles: Handle[] = []
  protected get vertices() {
    return this.cellView.cell.getVertices()
  }

  protected onRender() {
    this.addClass(this.prefixClassName('edge-tool-vertices'))
    if (this.options.addable) {
      this.updatePath()
    }
    this.resetHandles()
    this.renderHandles()
    return this
  }

  update() {
    const vertices = this.vertices
    if (vertices.length === this.handles.length) {
      this.updateHandles()
    } else {
      this.resetHandles()
      this.renderHandles()
    }

    if (this.options.addable) {
      this.updatePath()
    }

    return this
  }

  protected resetHandles() {
    const handles = this.handles
    this.handles = []
    if (handles) {
      handles.forEach((handle) => {
        this.stopHandleListening(handle)
        handle.remove()
      })
    }
  }

  protected renderHandles() {
    const vertices = this.vertices
    for (let i = 0, l = vertices.length; i < l; i += 1) {
      const vertex = vertices[i]
      const createHandle = this.options.createHandle!
      const processHandle = this.options.processHandle
      const handle = createHandle({
        index: i,
        graph: this.graph,
        guard: (evt: Dom.EventObject) => this.guard(evt), // eslint-disable-line no-loop-func
        attrs: this.options.attrs || {},
      })

      if (processHandle) {
        processHandle(handle)
      }

      handle.updatePosition(vertex.x, vertex.y)
      this.stamp(handle.container)
      this.container.appendChild(handle.container)
      this.handles.push(handle)
      this.startHandleListening(handle)
    }
  }

  protected updateHandles() {
    const vertices = this.vertices
    for (let i = 0, l = vertices.length; i < l; i += 1) {
      const vertex = vertices[i]
      const handle = this.handles[i]
      if (handle) {
        handle.updatePosition(vertex.x, vertex.y)
      }
    }
  }

  protected updatePath() {
    const connection = this.childNodes.connection
    if (connection) {
      connection.setAttribute('d', this.cellView.getConnectionPathData())
    }
  }

  protected startHandleListening(handle: Handle) {
    const edgeView = this.cellView
    if (edgeView.can('vertexMovable')) {
      handle.on('change', this.onHandleChange, this)
      handle.on('changing', this.onHandleChanging, this)
      handle.on('changed', this.onHandleChanged, this)
    }

    if (edgeView.can('vertexDeletable')) {
      handle.on('remove', this.onHandleRemove, this)
    }
  }

  protected stopHandleListening(handle: Handle) {
    const edgeView = this.cellView
    if (edgeView.can('vertexMovable')) {
      handle.off('change', this.onHandleChange, this)
      handle.off('changing', this.onHandleChanging, this)
      handle.off('changed', this.onHandleChanged, this)
    }

    if (edgeView.can('vertexDeletable')) {
      handle.off('remove', this.onHandleRemove, this)
    }
  }

  protected getNeighborPoints(index: number) {
    const edgeView = this.cellView
    const vertices = this.vertices
    const prev = index > 0 ? vertices[index - 1] : edgeView.sourceAnchor
    const next =
      index < vertices.length - 1 ? vertices[index + 1] : edgeView.targetAnchor
    return {
      prev: Point.create(prev),
      next: Point.create(next),
    }
  }

  protected getMouseEventArgs<T extends Dom.EventObject>(evt: T) {
    const e = this.normalizeEvent(evt)
    const { x, y } = this.graph.snapToGrid(e.clientX!, e.clientY!)
    return { e, x, y }
  }

  protected onHandleChange({ e }: EventArgs['change']) {
    this.focus()
    const edgeView = this.cellView
    edgeView.cell.startBatch('move-vertex', { ui: true, toolId: this.cid })
    if (!this.options.stopPropagation) {
      const { e: evt, x, y } = this.getMouseEventArgs(e)
      this.eventData(evt, { start: { x, y } })
      edgeView.notifyMouseDown(evt, x, y)
    }
  }

  protected onHandleChanging({ handle, e }: EventArgs['changing']) {
    const edgeView = this.cellView
    const index = handle.options.index
    const { e: evt, x, y } = this.getMouseEventArgs(e)
    const vertex = { x, y }
    this.snapVertex(vertex, index)
    edgeView.cell.setVertexAt(index, vertex, { ui: true, toolId: this.cid })
    handle.updatePosition(vertex.x, vertex.y)
    if (!this.options.stopPropagation) {
      edgeView.notifyMouseMove(evt, x, y)
    }
  }

  protected stopBatch(vertexAdded: boolean) {
    this.cell.stopBatch('move-vertex', { ui: true, toolId: this.cid })
    if (vertexAdded) {
      this.cell.stopBatch('add-vertex', { ui: true, toolId: this.cid })
    }
  }

  protected onHandleChanged({ e }: EventArgs['changed']) {
    const options = this.options
    const edgeView = this.cellView

    if (options.addable) {
      this.updatePath()
    }

    if (!options.removeRedundancies) {
      this.stopBatch(this.eventData(e).vertexAdded)
      return
    }

    const verticesRemoved = edgeView.removeRedundantLinearVertices({
      ui: true,
      toolId: this.cid,
    })

    if (verticesRemoved) {
      this.render()
    }

    this.blur()

    this.stopBatch(this.eventData(e).vertexAdded)

    const { e: evt, x, y } = this.getMouseEventArgs(e)

    if (!this.options.stopPropagation) {
      edgeView.notifyMouseUp(evt, x, y)
      const { start } = this.eventData(evt)
      if (start) {
        const { x: startX, y: startY } = start
        if (startX === x && startY === y) {
          edgeView.onClick(evt as unknown as Dom.ClickEvent, x, y)
        }
      }
    }

    edgeView.checkMouseleave(evt)

    options.onChanged && options.onChanged({ edge: edgeView.cell, edgeView })
  }

  protected snapVertex(vertex: PointLike, index: number) {
    const snapRadius = this.options.snapRadius || 0
    if (snapRadius > 0) {
      const neighbors = this.getNeighborPoints(index)
      const prev = neighbors.prev
      const next = neighbors.next
      if (Math.abs(vertex.x - prev.x) < snapRadius) {
        vertex.x = prev.x
      } else if (Math.abs(vertex.x - next.x) < snapRadius) {
        vertex.x = next.x
      }

      if (Math.abs(vertex.y - prev.y) < snapRadius) {
        vertex.y = neighbors.prev.y
      } else if (Math.abs(vertex.y - next.y) < snapRadius) {
        vertex.y = next.y
      }
    }
  }

  protected onHandleRemove({ handle, e }: EventArgs['remove']) {
    if (this.options.removable) {
      const index = handle.options.index
      const edgeView = this.cellView
      edgeView.cell.removeVertexAt(index, { ui: true })
      if (this.options.addable) {
        this.updatePath()
      }
      edgeView.checkMouseleave(this.normalizeEvent(e))
    }
  }

  protected allowAddVertex(e: Dom.MouseDownEvent) {
    const guard = this.guard(e)
    const addable = this.options.addable && this.cellView.can('vertexAddable')
    const matchModifiers = this.options.modifiers
      ? isModifierKeyMatch(e, this.options.modifiers)
      : true
    return !guard && addable && matchModifiers
  }

  protected onPathMouseDown(evt: Dom.MouseDownEvent) {
    const edgeView = this.cellView

    if (!this.allowAddVertex(evt)) {
      return
    }

    evt.stopPropagation()
    evt.preventDefault()

    const e = this.normalizeEvent(evt)
    const vertex = this.graph.snapToGrid(e.clientX, e.clientY).toJSON()
    edgeView.cell.startBatch('add-vertex', { ui: true, toolId: this.cid })
    const index = edgeView.getVertexIndex(vertex.x, vertex.y)
    this.snapVertex(vertex, index)
    edgeView.cell.insertVertex(vertex, index, {
      ui: true,
      toolId: this.cid,
    })
    this.render()
    const handle = this.handles[index]
    this.eventData(e, { vertexAdded: true })
    handle.onMouseDown(e)
  }

  protected onRemove() {
    this.resetHandles()
  }
}

interface Options extends ToolItemOptions {
  snapRadius?: number
  addable?: boolean
  removable?: boolean
  removeRedundancies?: boolean
  stopPropagation?: boolean
  modifiers?: string | ModifierKey[]
  attrs?: SimpleAttrs | ((handle: Handle) => SimpleAttrs)
  createHandle?: (options: HandleOptions) => Handle
  processHandle?: (handle: Handle) => void
  onChanged?: (options: { edge: Edge; edgeView: EdgeView }) => void
}

export class Handle extends View<EventArgs> {
  protected get graph() {
    return this.options.graph
  }

  constructor(public readonly options: HandleOptions) {
    super()
    this.render()
    this.delegateEvents({
      mousedown: 'onMouseDown',
      touchstart: 'onMouseDown',
      dblclick: 'onDoubleClick',
    })
  }

  render() {
    this.container = createViewElement('circle', true)
    const attrs = this.options.attrs
    if (typeof attrs === 'function') {
      const defaults = Vertices.getDefaults<Options>()
      this.setAttrs({
        ...defaults.attrs,
        ...attrs(this),
      })
    } else {
      this.setAttrs(attrs)
    }

    this.addClass(this.prefixClassName('edge-tool-vertex'))
  }

  updatePosition(x: number, y: number) {
    this.setAttrs({ cx: x, cy: y })
  }

  onMouseDown(evt: Dom.MouseDownEvent) {
    if (this.options.guard(evt)) {
      return
    }

    evt.stopPropagation()
    evt.preventDefault()
    this.graph.view.undelegateEvents()

    this.delegateDocumentEvents(
      {
        mousemove: 'onMouseMove',
        touchmove: 'onMouseMove',
        mouseup: 'onMouseUp',
        touchend: 'onMouseUp',
        touchcancel: 'onMouseUp',
      },
      evt.data,
    )

    this.emit('change', { e: evt, handle: this })
  }

  protected onMouseMove(evt: Dom.MouseMoveEvent) {
    this.emit('changing', { e: evt, handle: this })
  }

  protected onMouseUp(evt: Dom.MouseUpEvent) {
    this.emit('changed', { e: evt, handle: this })
    this.undelegateDocumentEvents()
    this.graph.view.delegateEvents()
  }

  protected onDoubleClick(evt: Dom.DoubleClickEvent) {
    this.emit('remove', { e: evt, handle: this })
  }
}

interface HandleOptions {
  graph: Graph
  index: number
  guard: (evt: Dom.EventObject) => boolean
  attrs: SimpleAttrs | ((handle: Handle) => SimpleAttrs)
}

interface EventArgs {
  change: { e: Dom.MouseDownEvent; handle: Handle }
  changing: { e: Dom.MouseMoveEvent; handle: Handle }
  changed: { e: Dom.MouseUpEvent; handle: Handle }
  remove: { e: Dom.DoubleClickEvent; handle: Handle }
}
